Starship Troopers |
A different approach to UI5 tests execution
Presented by Arnaud Buchholz
|
Innovation sometimes requires ignoring recommendations 😅
Back to 2018...
A journey with OPA
recording training-ui5con18-opaUse UI5 to test your ODATA service
recording node-ui5Got the trend ?
If it's not tested, it does not work
Need I say more ?
Selenium, Marionette or Puppeteer are framework agnostic, one must rely on the generated HTML to code automation. They are designed for End 2 End testing.
OPA is designed by and for UI5 developers: one manipulates UI5 controls.
Tests execution
npm start
These tests can be automated in a pipeline to validate the code before merging and / or shipping the new version.
You use Karma
You understand how Karma executes the tests
... I do not understand 😱
Karma tests runner
npm run karma
Karma tests runner (CI)
npm run karma-ci
sap-ui-core.js
<script id="sap-ui-bootstrap" src="../resources/sap-ui-core.js"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-libs="sap.m"
data-sap-ui-resourceroots='{"Demo": "./"}'
data-sap-ui-onInit="module:Demo/index"
data-sap-ui-async="true">
</script>
/resources/
or /test-resources/
neo-app.json
<script id="sap-ui-bootstrap"
src="https://openui5.hana.ondemand.com/1.87.0/resources/sap-ui-core.js"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-libs="sap.m"
data-sap-ui-resourceroots='{"Demo": "./"}'
data-sap-ui-onInit="module:Demo/index"
data-sap-ui-async="true">
</script>
Can we combine a lightweight setup
with a fast & external version selection ?
...In an automatable way ?
Small, configurable and reusable
{
"port": 8080,
"mappings": [{
"match": "/(test-)?resources/(.*)",
"url": "https://openui5.hana.ondemand.com/1.87.0/$1resources/$2"
}, {
"match": "^/(.*)",
"file": "./webapp/$1"
}]
}
Serving an UI5 application with REserve
npm run reserve
Every aspect of the platform is
configurable through options :
webapp
folderUI5
CDNDone by embedding REserve
const { check, serve } = require('reserve')
check({
port: job.port,
mappings: [{
match: '/(test-)?resources/(.*)', // UI5 mapping
url: `${job.ui5}/$1`
}, {
match: /^\/(.*)/, // Project mapping
file: join(job.webapp, '$1')
}]
})
.then(configuration => serve(configuration))
}
To automate the tests execution, the platform needs a way to start a browser and stop it when the tests are done (or the timeout is reached).
The browser instantiation is deferred to a configurable script that is spawned by the platform.
By default, a puppeteer
script is provided
const puppeteer = require('puppeteer')
let browser
process.on('message', async message => {
if (message.command === 'stop') { // End signal
await browser.close()
process.exit(0)
}
})
async function main () {
browser = await puppeteer.launch({
headless: true,
args: [process.argv[2] /* Test page to open */, /* ... */]
})
}
main()
Tests were conducted to validate that
multiple and concurrent instances
do not interfere with each other :
During the tests execution, the platform needs to receive feedback from the browsers.
For instance : to know when a test ends
Dedicated endpoints are created.
(base URL : /_/
)
The endpoints leverage the referer
header to
identify which test (test page URL) initiated the request.
const { body } = require('reserve')
function endpoint (implementation) {
return async function (request, response) {
response.writeHead(200)
response.end()
const url = request.headers.referer
const data = JSON.parse(await body(request))
try {
await implementation.call(this, url, data)
} catch (e) {
console.error(`Exception when processing ${url}`, e)
}
}
}
Serving an UI5 application with the platform
ui5-test-runner -parallel:0 -port:8080
The test suite page
declares every unit and OPA tests
contained in the project.
webapp/test/testsuite.qunit.html
When opening the page, a redirection occurs
and a runner executes the declared tests.
<html>
<head>
<script src="../resources/sap/ui/qunit/qunit-redirect.js"></script>
<script>
function suite() {
var oSuite = new parent.jsUnitTestSuite(),
iLastSep = location.pathname.lastIndexOf("/") + 1,
sContextPath = location.pathname.substring(0, iLastSep);
oSuite.addTestPage(sContextPath + "unit/unitTests.qunit.html");
oSuite.addTestPage(sContextPath + "integration/opaTests.qunit.html");
return oSuite;
}
</script>
</head>
</html>
testsuite.qunit.html
By substituting qunit-redirect.js
with a custom script and
opening testsuite.qunit.html
,
the platform captures the list of test pages.
const pages = []
function jsUnitTestSuite () {}
jsUnitTestSuite.prototype.addTestPage = function (url) {
pages.push(url)
}
window.jsUnitTestSuite = jsUnitTestSuite // Expose
window.addEventListener('load', function () {
suite() // Trigger
const xhr = new XMLHttpRequest()
xhr.open('POST', '/_/addTestPages')
xhr.send(JSON.stringify(pages))
})
qunit-redirect.js
used to extract test pagesSubstituting qunit-redirect.js
ui5-test-runner -parallel:-1 -port:8080
qunit-redirect.js
qunit-redirect.js
The OPA framework is built on top of QUnit
.
QUnit
exposes hooks to monitor the tests :
They provide information about
progress and status.
When the test page loads the QUnit
module,
the platform injects code to leverage the hooks
and trigger specific endpoints.
qunit.js
qunit-2.js
After probing the tests, the platform knows the list of test page URL.
Then, it instantiates concurrent browsers to run each page individually.
The number of concurrent browsers is given by the option parallel
.
The test page is stopped when the test is done (QUnit.testDone
) or when the timeout is reached.
for (let i = 0; i < job.parallel; ++i) runTestPage()
async function runTestPage () {
const { length } = job.testPageUrls
if (job.testPagesCompleted === length) {
return generateReport() // Last test completed
}
if (job.testPagesStarted === length) {
return // No more tests to run
}
const url = job.testPageUrls[job.testPagesStarted++]
await start(url) // Resolved when the browser is stopped (or timed out)
++job.testPagesCompleted
runTestPage()
}
Executing the tests in parallel
ui5-test-runner -parallel:2 -port:8080 -cache:.ui5 -keepAlive:true
The process requires three steps :
nyc
is a command line wrapper for Istanbul
,
a javascript code coverage tool.
The platform leverage nyc
by spawning commands and waiting for their termination.
The instrumentation step is triggered before executing the tests. By default, the test files are excluded from the coverage report.
NYC aggregates the collected coverage information at the window level. Because of the IFrame usage in OPA, this is changed to the top window by the platform.
When an instrumented file exists for a source file, it is substituted.
/* Reserve mappings : */ [{
match: /^\/(.*\.js)$/,
file: join(instrumentedSourceDir, '$1'),
'ignore-if-not-found': true
}, {
match: /^\/(.*)/,
file: join(job.webapp, '$1')
}]
Each test page is executed separately : corresponding coverage data is saved at the end of each test page.
When all the tests are completed, NYC is used to merge the coverage information and generate the coverage report.
Coverage measurement
ui5-test-runner -parallel:2 -port:8080 -cache:.ui5 -keepAlive:true
ui5-test-runner
training-ui5Con18-opa
)